================================== Extending RISE through paradigms ================================== This chapter describes how RISE separates *what* a model is from *how* it is solved, through the notion of a **paradigm**. A paradigm is a self-contained computational strategy -- perturbation, recursive local linearization (RLL), :math:`k`-th order Taylor projection (TP), occasionally-binding-constraint (occbin) piecewise linearization, and so on -- that plugs into mainland RISE through a small, fixed interface. The aim is to add advanced solution methods **without modifying RISE's core** and without paradigms interfering with one another. This chapter is primarily for developers extending RISE. The three paradigms currently targeted by this framework are: #. **occbin** -- piecewise-linear, OccBin-style occasionally-binding constraints (being migrated out of mainland RISE into this framework); #. **recursive local linearization (RLL)**; #. :math:`k`-**th order Taylor projection (TP)**. Goals ===== The paradigm architecture is designed to deliver: - a clean separation between model *structure* and computational *method*; - support for multiple parameterizations of the same model; - extensibility to advanced methods without touching RISE core; - prevention of cross-talk between parameterizations; - strictly read-only access to RISE internals by paradigm code. A model contains **exactly one paradigm** at a time, but may carry **multiple parameter vectors**, each producing its own solution object. What a paradigm is ================== A paradigm :math:`\mathcal{P}` is a computational universe that #. solves a parameterized model :math:`m(\theta)`, #. defines the internal representation of the solution :math:`S(\theta)`, and #. provides operators acting on that solution (e.g. a one-step forecast). It is **not** the model, **not** a parameter vector, and **not** a container for multiple solutions. Formally, .. math:: S(\theta) = \mathcal{P}.\texttt{solve\_engine}\bigl(m(\theta),\, \texttt{regular\_solver}\bigr), where :math:`S(\theta)` is *partly transparent* to RISE (through the standard envelope below) and *partly opaque* (through the ``S.solution`` payload). Responsibilities of a paradigm ============================== A paradigm **must**: - operate on a single parameter vector at a time; - return a single solution object ``S`` conforming to the standard envelope; - provide operators derived from that solution; - remain unaware of parameter multiplicity; - treat the model object ``M`` as **read-only** -- it may call ``regular_solver(M, ...)`` to obtain derivatives, but must never modify ``M`` nor return a modified ``M`` to RISE; - avoid modifying RISE core structures or global options. A paradigm **may** use RISE's baseline solver internally, use RISE's constraint enforcement, and keep mutable internal state inside ``S.solution`` (see `Mutable solution objects`_). A paradigm **must not** store global state across parameterizations, return a modified ``M``, or manage arrays of solutions. The standard solution envelope ============================== Every paradigm's ``solve_engine`` returns a struct ``S`` with the fields below. This *standard envelope* lets RISE orchestrate the workflow (convergence checking, re-solve decisions, filter routing) **without knowing anything about the paradigm's internals**. .. list-table:: :header-rows: 1 :widths: 18 18 64 * - Field - Type - Meaning * - ``is_solved`` - ``[1x2 logical]`` - ``[ss_ok, dynamics_ok]``: first entry, the steady state / baseline solve succeeded; second entry, the dynamics / projection solve succeeded. * - ``warrant_resolving`` - ``logical`` - ``true`` if the solve failed or degraded and RISE should attempt a re-solve. * - ``retcode`` - ``integer`` - ``0`` = success (RISE's usual ``retcode`` convention). * - ``H`` - matrix or ``[]`` - measurement-error covariance for the Kalman filter; ``[]`` if the paradigm provides none. * - ``solution`` - handle object - opaque, paradigm-owned payload. RISE stores it but never inspects it. All paradigm-specific state lives here. The envelope is deliberately minimal. Fields specific to perturbation theory (eigenvalues, number of solutions, ``Tz``, ...) are **not** required, because they are meaningless or misleading for paradigms such as RLL and TP; those paradigms keep such quantities inside ``S.solution``. A paradigm should start from a *failed* default and populate fields as each stage succeeds, so partial failures are reported correctly:: function S = make_envelope() S.is_solved = [false, false]; S.warrant_resolving = true; S.retcode = 1; S.H = []; S.solution = []; end The minimum interface contract ============================== A paradigm exposes an ``engines`` struct with these fields. **name** (required) A short string identifier, e.g. ``'perturbation'``, ``'rll'``, ``'rtp'``. **solve_engine** (required) :: S = solve_engine(M, regular_solver) ``M`` is **read-only**: the paradigm may call ``regular_solver(M, ...)`` internally, but the solved model is used internally only and never returned. ``S`` must conform to the standard envelope, and no global or shared state may be modified. **one_step_engine** (required) :: one_step = one_step_engine(S) The *only* input is ``S`` -- the model ``M`` is **not** passed here, so everything the closure needs must have been captured in ``S.solution`` during ``solve_engine``. It returns a closure :: [x_t, retcode, extra...] = one_step(x_tm1, eps_t, regime) with ``x_tm1`` the lagged endogenous vector :math:`x_{t-1}`, ``eps_t`` the shock vector :math:`\varepsilon_t`, and ``regime`` the integer index of the current Markov regime. It returns a nonzero ``retcode`` if the step fails; ``extra...`` are optional outputs such as the local linear coefficients ``(k, H, G)`` consumed by the smoother. **filter_engine** (optional) Future extension for filtering / smoothing. **use_rise_constraint_enforcer** (optional) Logical, default ``true``. Mutable solution objects ======================== For RLL and TP the effective policy function is *state-dependent*: it is updated at each simulation step as the economy evolves. This conflicts with RISE's assumption that the stored solution ``S`` is static. The recommended resolution is to implement ``S.solution`` as a MATLAB **handle class**. Because handle objects are passed by reference, the ``one_step`` closure and RISE's stored copy of ``S.solution`` point to the *same* object: when ``one_step`` updates it in place, RISE's copy reflects the change automatically, without RISE needing to know an update occurred. :: classdef SolutionHandle < handle properties % paradigm-specific fields populated by solve_engine, e.g. T % TP: current polynomial coefficients k, H, G % RLL: current local linear policy % ... end end The workflow is: #. ``solve_engine`` constructs a fresh ``SolutionHandle``, populates it, and stores it as ``S.solution``; #. ``one_step_engine(S)`` extracts ``payload = S.solution`` and builds a closure capturing ``payload`` by reference; #. at each step the closure updates ``payload`` in place; RISE's stored ``S.solution`` reflects the updates automatically; #. RISE reads only the envelope fields and never the payload. .. note:: Because RISE never reads ``S.solution`` directly, this pattern is safe. The only contract is that ``one_step(x_tm1, eps_t, regime)`` always returns a valid ``x_t`` and ``retcode``, whatever the payload's internal state. Responsibilities of RISE ======================== RISE manages model structure and parameter multiplicity; calls ``solve_engine(M, regular_solver)`` for each :math:`\theta_i` (passing ``M`` read-only); stores each :math:`S_i` in ``model.solution(i)``; and routes simulation, forecasting and filtering through the closures returned by ``one_step_engine`` (and ``filter_engine``). The workflow is .. math:: \theta_i \;\rightarrow\; m(\theta_i) \;\rightarrow\; S_i = \texttt{solve\_engine}(m(\theta_i),\,\texttt{regular\_solver}) \;\rightarrow\; \text{store } S_i \;\rightarrow\; \text{simulation / filtering}. RISE must **not** inspect or modify ``S.solution``, assume any format inside it, or reuse a ``one_step`` closure across different parameter vectors. Isolation, safety and persistence ================================== To prevent cross-talk between parameterizations, each call to ``solve_engine`` must construct a **fresh** ``SolutionHandle`` (copying a handle copies only the reference, not the data), no paradigm may keep persistent state outside ``S.solution``, and RISE must not reuse engines across parameter vectors unless the paradigm is stateless. For saving models to disk, paradigms should provide serialization hooks :: blob = serialize_solution(S) S = deserialize_solution(blob) which RISE stores without inspecting. Examples ======== Recursive local linearization (RLL) ----------------------------------- - ``solve_engine`` calls ``regular_solver`` read-only, extracts the baseline local policy :math:`(k_0, H_0, G_0)`, stores everything in an ``rll.SolutionHandle``, and returns the standard envelope with ``S.solution = payload``. - ``one_step_engine(S)`` returns a closure that builds a zero-shock stance, re-evaluates the structural derivatives at that stance, solves the local linear system for updated :math:`(k_t, H_t, G_t)`, updates the payload in place, and returns :math:`x_t` together with :math:`(k_t, H_t, G_t)` for the smoother. :math:`k`-th order Taylor projection (TP) ----------------------------------------- - ``solve_engine`` calls ``regular_solver`` read-only, uses the perturbation solution as a warm start, runs ``rtp.solve`` to obtain :math:`T^* = \{T_1^*,\dots,T_h^*\}`, and stores :math:`T^*`, the topology and ``get_F`` in an ``rtp.SolutionHandle``. - ``one_step_engine(S)`` returns a closure that builds the expansion point :math:`z_{\text{anchor}} = (p_{t-1}, b_{t-1}, \sigma{=}0, \varepsilon{=}0)`, re-solves ``rtp.solve`` around it (warm-started from the payload), updates the payload in place, and evaluates .. math:: x_t = \sum_{q=0}^{K} T^*_{\text{regime}}\{q{+}1\}\, \bigl(z_t - z_{\text{anchor}}\bigr)^{\otimes q}, with :math:`z_t = (p_{t-1}, b_{t-1}, \sigma{=}1, \varepsilon_t)`. Design principles ================= - A model has exactly one paradigm. - A model may have multiple parameterizations; each has one solution ``S``. - ``M`` is always read-only inside a paradigm. - ``S.solution`` is always opaque to RISE. - The envelope gives RISE just enough to orchestrate workflows. - Mutable simulation-time state lives inside ``S.solution`` via handle semantics. - Paradigm-specific solution concepts (eigenvalues, number of solutions, Taylor coefficients) live inside ``S.solution``, not in the envelope.